#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <iostream>
#include <signal.h>
#include <vector>
#include <stdarg.h>
#include <arpa/inet.h>
#include "main.hpp"
#include "utils.hpp"
#include "stats.hpp"
#include "hub-server.hpp"

static uint32_t xlln_debuglog_level = XLLN_LOG_CONTEXT_MASK | XLLN_LOG_LEVEL_WARN | XLLN_LOG_LEVEL_ERROR | XLLN_LOG_LEVEL_FATAL;

uint32_t XLLNDebugLogF(uint32_t logLevel, const char *const format, ...)
{
	if (!(logLevel & xlln_debuglog_level & XLLN_LOG_CONTEXT_MASK) || !(logLevel & xlln_debuglog_level & XLLN_LOG_LEVEL_MASK)) {
		return 0;
	}
	
	auto temp = std::vector<char>{};
	auto length = std::size_t{ 63 };
	va_list args;
	while (temp.size() <= length) {
		temp.resize(length + 1);
		va_start(args, format);
		const auto status = vsnprintf(temp.data(), temp.size(), format, args);
		va_end(args);
		if (status < 0) {
			// string formatting error.
			return EINVAL;
		}
		length = static_cast<std::size_t>(status);
	}
	printf("%s\n", std::string{ temp.data(), length }.c_str());
	return 0;
}

namespace EXEC_FLAGS {
	enum Type : uint8_t {
		UNKNOWN = 0,
		VERSION,
		HELP,
		TEST,
		HOST_ADDR,
		HOST_PORT,
		STATS_FILE,
	};
}

static EXEC_FLAGS::Type WhichFlag(char *arg)
{
	size_t argFlagLen = strlen(arg);
	char *equals = strchr(arg, '=');
	if (equals) {
		argFlagLen = (equals - arg);
	}
	
	if (strncasecmp("-V", arg, argFlagLen) == 0 || strncasecmp("--version", arg, argFlagLen) == 0) {
		return EXEC_FLAGS::VERSION;
	}
	else if (strncasecmp("-I", arg, argFlagLen) == 0 || strncasecmp("--hostaddr", arg, argFlagLen) == 0) {
		return EXEC_FLAGS::HOST_ADDR;
	}
	else if (strncasecmp("-P", arg, argFlagLen) == 0 || strncasecmp("--hostport", arg, argFlagLen) == 0) {
		return EXEC_FLAGS::HOST_PORT;
	}
	else if (strncasecmp("-S", arg, argFlagLen) == 0 || strncasecmp("--statsfile", arg, argFlagLen) == 0) {
		return EXEC_FLAGS::STATS_FILE;
	}
	else if (strncasecmp("-T", arg, argFlagLen) == 0 || strncasecmp("--test", arg, argFlagLen) == 0) {
		return EXEC_FLAGS::TEST;
	}
	else if (strncasecmp("-H", arg, argFlagLen) == 0
		|| strncasecmp("/H", arg, argFlagLen) == 0
		|| strncasecmp("-help", arg, argFlagLen) == 0
		|| strncasecmp("--help", arg, argFlagLen) == 0
		|| strncasecmp("/help", arg, argFlagLen) == 0
		|| strncasecmp("/?", arg, argFlagLen) == 0
		|| strncasecmp("-?", arg, argFlagLen) == 0
		|| strncasecmp("--?", arg, argFlagLen) == 0
	) {
		return EXEC_FLAGS::HELP;
	}
	
	return EXEC_FLAGS::UNKNOWN;
}

/// Locked for the duration of the programs life. Unlocked when the main thread wants to shutdown.
pthread_mutex_t xlln_mutex_exit_threads;

static bool allowEofExit = false;

static void SignalHandler(int sig)
{
	XLLN_DEBUG_LOG(XLLN_LOG_CONTEXT_XLIVELESSNESS | XLLN_LOG_LEVEL_INFO
		, "\nCaught signal %d."
		, sig
	);
	
	allowEofExit = true;
}

static void InterpretInputCommands()
{
	char inputCommand[800];
	while (1) {
		
		printf("> ");
		if (!fgets(inputCommand, 800, stdin)) {
			if (allowEofExit) {
				break;
			}
			continue;
		}
		
		size_t inputCommandLen = strnlen(inputCommand, 800);
		
		if (inputCommandLen > 0) {
			if (inputCommand[inputCommandLen - 1] == '\n') {
				inputCommand[(inputCommandLen--) - 1] = 0;
			}
		}
		
		if (inputCommandLen == 0) {
			continue;
		}
		else if (
			strcasecmp("exit", inputCommand) == 0
			|| strcasecmp("q", inputCommand) == 0
			|| strcasecmp("quit", inputCommand) == 0
		) {
			// Remove the signal handler since we are exiting now anyway and if there is a problem the first kill signal should end it.
			struct sigaction sigIntHandler;
			sigemptyset(&sigIntHandler.sa_mask);
			sigIntHandler.sa_flags = 0;
			sigIntHandler.sa_handler = SIG_DFL;
			sigaction(SIGINT, &sigIntHandler, NULL);
			
			break;
		}
		else if (
			strcasecmp("help", inputCommand) == 0
			|| strcasecmp("h", inputCommand) == 0
			|| strcasecmp("commands", inputCommand) == 0
			|| strcasecmp("/help", inputCommand) == 0
			|| strcasecmp("/h", inputCommand) == 0
			|| strcasecmp("-help", inputCommand) == 0
			|| strcasecmp("-h", inputCommand) == 0
			|| strcasecmp("--help", inputCommand) == 0
			|| strcasecmp("--h", inputCommand) == 0
		) {
			printf("exit, quit, q - Quit the program gracefully (the 1st CTRL+C keystroke will also do this, the 2nd will kill).\n");
			printf("help - Prints this help dialog.\n");
			printf("version - Prints out the version of this XLiveLessNess Hub server.\n");
			printf("count - The number of XLLN instances connected.\n");
			printf("versions - The number of each XLLN version that is connected.\n");
			printf("titles - The number of each Titles/games that are connected.\n");
			printf("titleversions - The number of each Title/game and their version that are connected.\n");
		}
		else if (strcasecmp("version", inputCommand) == 0) {
			printf("XLLN-Hub version: %hhu.%hhu.%hhu.%hhu.\n", (uint8_t)VERSION_MAJOR, (uint8_t)VERSION_MINOR, (uint8_t)VERSION_REVISION, (uint8_t)VERSION_BUILD);
		}
		else if (strcasecmp("count", inputCommand) == 0) {
			size_t numOfConnectedInstances = 0;
			size_t numOfAdditionalSockets = 0;
			size_t numOfRecentlyIgnoredUnknownAddrs = 0;
			GetStatsConnectedEntities(&numOfConnectedInstances, &numOfAdditionalSockets, &numOfRecentlyIgnoredUnknownAddrs);
			
			printf("Number of XLLN Instances connected: %zu.\n", numOfConnectedInstances);
			printf("Number of additional sockets connected: %zu.\n", numOfAdditionalSockets);
			printf("Number of recently ignored unknown addresses: %zu.\n", numOfRecentlyIgnoredUnknownAddrs);
		}
		else if (strcasecmp("versions", inputCommand) == 0) {
			std::map<uint32_t, size_t> versions;
			GetStatsXllnVersion(versions);
			
			if (versions.size()) {
				printf("XLLN Versions   : Count\n");
				for (auto const &version : versions) {
					char strVersion[16];
					int numWritten = snprintf(strVersion, 16, "%hhu.%hhu.%hhu.%hhu", (uint8_t)(version.first >> 24), (uint8_t)((version.first >> 16) & 0xFF), (uint8_t)((version.first >> 8) & 0xFF), (uint8_t)(version.first & 0xFF));
					for (uint8_t i = numWritten; i < 15; i++) {
						strVersion[i] = ' ';
					}
					strVersion[15] = 0;
					printf("%s : %zu\n", strVersion, version.second);
				}
			}
			else {
				printf("No active Instances.\n");
			}
		}
		else if (strcasecmp("titleversions", inputCommand) == 0) {
			std::map<uint64_t, size_t> versions;
			GetStatsTitleVersions(versions);
			
			if (versions.size()) {
				printf("Title    : Version    : Count\n");
				for (auto const &version : versions) {
					printf("%08X : 0x%08x : %zu\n", (uint32_t)(version.first >> 32), (uint32_t)(version.first & 0xFFFFFFFF), version.second);
				}
			}
			else {
				printf("No active Instances.\n");
			}
		}
		else if (strcasecmp("titles", inputCommand) == 0) {
			std::map<uint64_t, size_t> titleVersions;
			GetStatsTitleVersions(titleVersions);
			
			std::map<uint32_t, size_t> titles;
			for (auto const &titleVersion : titleVersions) {
				uint32_t titleId = (uint32_t)(titleVersion.first >> 32);
				auto const currentCountPair = titles.find(titleId);
				size_t currentCount;
				if (currentCountPair == titles.end()) {
					currentCount = 1;
				}
				else {
					currentCount = currentCountPair->second + 1;
				}
				titles[titleId] = currentCount;
			}
			
			if (titleVersions.size()) {
				printf("Title    : Count\n");
				for (auto const &title : titles) {
					printf("%08X : %zu\n", title.first, title.second);
				}
			}
			else {
				printf("No active Instances.\n");
			}
		}
		else {
			printf("Unknown Command.\n");
		}
		
	}
}

int main(int argc, char **argv)
{
	int result = EXIT_SUCCESS;
	
	uint16_t xlln_port_base_0_HBO = 1100;
	xlln_sockaddr_base_0.ss_family = AF_INET;
	((sockaddr_in*)&xlln_sockaddr_base_0)->sin_addr.s_addr = htonl(INADDR_ANY);
	
	bool abort = false;
	bool printHelp = false;
	bool testMode = false;
	
	#define ExecFlagExit argi = argc; abort = true; break;
	#define ExecFlagExitFailure argi = argc; result = EXIT_FAILURE; break;
	#define ExecFlagExitFailurePrintHelp printHelp = true; ExecFlagExitFailure;
	#define RequireExecFlagValue(flagValue) {\
		flagValue = strchr(argv[argi], '=');\
		if (!flagValue) {\
			printf("%s requires an additional parameter.\n", argv[argi]);\
			ExecFlagExitFailurePrintHelp;\
		}\
		flagValue++;\
	}
	
	for (int argi = 1; argi < argc; argi++) {
		EXEC_FLAGS::Type execFlag = WhichFlag(argv[argi]);
		switch(execFlag) {
			case EXEC_FLAGS::VERSION: {
				printf("XLLN-Hub version: %hhu.%hhu.%hhu.%hhu.\n", (uint8_t)VERSION_MAJOR, (uint8_t)VERSION_MINOR, (uint8_t)VERSION_REVISION, (uint8_t)VERSION_BUILD);
				ExecFlagExit;
			}
			case EXEC_FLAGS::HOST_ADDR: {
				char *flagValue;
				RequireExecFlagValue(flagValue);
				
				struct in_addr serveraddrv4;
				struct in6_addr serveraddrv6;
				if (inet_pton(AF_INET, flagValue, &serveraddrv4) == 1) {
					xlln_sockaddr_base_0.ss_family = AF_INET;
					((sockaddr_in*)&xlln_sockaddr_base_0)->sin_addr.s_addr = serveraddrv4.s_addr;
				}
				else if (inet_pton(AF_INET6, flagValue, &serveraddrv6) == 1) {
					xlln_sockaddr_base_0.ss_family = AF_INET6;
					((sockaddr_in6*)&xlln_sockaddr_base_0)->sin6_addr = serveraddrv6;
				}
				else {
					printf("Unable to parse the host address execution argument.\n");
					ExecFlagExitFailurePrintHelp;
				}
				break;
			}
			case EXEC_FLAGS::HOST_PORT: {
				char *flagValue;
				RequireExecFlagValue(flagValue);
				
				uint16_t hostPort = 0;
				if (sscanf(flagValue, "%hu", &hostPort) == 1 && hostPort > 0) {
					xlln_port_base_0_HBO = hostPort;
				}
				else {
					printf("Unable to parse the host port execution argument.\n");
					ExecFlagExitFailurePrintHelp;
				}
				break;
			}
			case EXEC_FLAGS::STATS_FILE: {
				char *flagValue;
				RequireExecFlagValue(flagValue);
				
				// char *statsPath = PathFromFilename(flagValue);
				// uint32_t errorMkdir = EnsureDirectoryExists(statsPath);
				stats_file = fopen(flagValue, "r+");
				if (!stats_file) {
					printf("Unable to open the stats file with error: %d.\n", errno);
					ExecFlagExitFailure;
				}
				if (false) {
					printf("Unable to parse path on the stats file argument.\n");
					ExecFlagExitFailurePrintHelp;
				}
				break;
			}
			case EXEC_FLAGS::TEST: {
				testMode = true;
				argi = argc;
				break;
			}
			case EXEC_FLAGS::HELP: {
				printHelp = true;
				ExecFlagExit
			}
			default: {
				printf("Unknown flag :\"%s\".\n", argv[argi]);
				ExecFlagExitFailurePrintHelp;
			}
		}
	}
	
	if (printHelp) {
		printf("Usage: xlln-hub [OPTION]...\n");
		printf(" -V,    --version               Prints out the version of this XLiveLessNess Hub server and exits.\n");
		printf(" -I=<>, --hostaddr=<address>    Sets the local network socket address to host on (IPv4 or IPv6).\n");
		printf(" -P=<>, --hostport=<port>       Sets the local network socket port to host on.\n");
		printf(" -S=<>, --statsfile=<filepath>  Sets the file to write stats to.\n");
		
#ifdef _DEBUG
		printf(" -T   , --test                  Do not use any of the other flags. We are running custom code.\n");
#endif
	}
	
	if (result || abort) {
		return result;
	}
	
	// Set the port number now as the sockaddr variable is done being potentially re-written when assigning the IP address to it.
	SetSockAddrPort(&xlln_sockaddr_base_0, xlln_port_base_0_HBO);
	
#ifdef _DEBUG
	if (testMode) {
		printf("=== TEST MODE ===\n");
		// raise(SIGTRAP);
		
		// struct timespec timeoutTime;
		// // clock_gettime(CLOCK_REALTIME, &timeoutTime);
		// timeoutTime.tv_sec = 10;
		// timeoutTime.tv_nsec = 0;
		// nanosleep(&timeoutTime, NULL);
		
		return result;
	}
#endif
	if (testMode) {
		return EXIT_FAILURE;
	}
	
	if (result = pthread_mutex_init(&xlln_mutex_exit_threads, NULL)) {
		XLLN_DEBUG_LOG(XLLN_LOG_CONTEXT_XLIVELESSNESS | XLLN_LOG_LEVEL_FATAL
			, "pthread_mutex_init failed on xlln_mutex_exit_threads with error %d."
			, result
		);
		return result;
	}
	pthread_mutex_lock(&xlln_mutex_exit_threads);
	
	int resultStats = InitStats();
	if (resultStats) {
		return resultStats;
	}
	int resultHubServer = InitHubServer();
	if (resultHubServer) {
		return resultHubServer;
	}
	
	{
		struct sigaction sigIntHandler;
		sigIntHandler.sa_handler = SignalHandler;
		sigemptyset(&sigIntHandler.sa_mask);
		// Single use / one shot.
		sigIntHandler.sa_flags = SA_RESETHAND;
		sigaction(SIGINT, &sigIntHandler, NULL);
	}
	
	InterpretInputCommands();
	
	pthread_mutex_unlock(&xlln_mutex_exit_threads);
	
	resultStats = UninitStats();
	resultHubServer = UninitHubServer();
	
	if (resultStats) {
		return resultStats;
	}
	if (resultHubServer) {
		return resultHubServer;
	}
	
	if (result = pthread_mutex_destroy(&xlln_mutex_exit_threads)) {
		XLLN_DEBUG_LOG(XLLN_LOG_CONTEXT_XLIVELESSNESS | XLLN_LOG_LEVEL_FATAL
			, "pthread_mutex_destroy failed on xlln_mutex_exit_threads with error %d."
			, result
		);
		return result;
	}
	
	return result;
}
